﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.Objects;
using System.Data.Objects.DataClasses;
using System.Linq;
using System.Reflection;
using System.Text;
using Fintech.EntityFrameworkEngine;
using Fintech.Utils;
using Fintech.Utils.Database;
using Fintech.Utils.Diagnostics;
using Microsoft.Data.Extensions;
using ZOVNP.Model.Abstracts;
using EntityState = ZOVNP.Model.Abstracts.EntityState;

namespace ZOVNP.Engine
{
    /// <summary> Класс, представляющий объектную модель базы данных Skif_BP, контекст работы. </summary>
    /// <remarks> В эту часть partial-класса необходимо писать код, чтобы он не был удалён генератором при следующем сохранении .edmx файла описания схемы хранения. </remarks>
    public partial class ZOVNPContext : EngineDrivenContext, IDomainContext
    {
        #region Классы

        /// <summary> Посетитель графа объектов, проставляющий им всем используемый ими контекст. </summary>
        private class AssignContextVisitor : EntityObjectGraphVisitor
        {
            private readonly ZOVNPContext context;
            internal AssignContextVisitor(ZOVNPContext Context) { context = Context; }
            /// <summary> Семантика посещения вершины графа. </summary>
            /// <param name="TargetEntity"> Посещаемая вершина. </param>
            /// <param name="SourceEntity"> Вершина, с которой был переход на посещаемую вершину. </param>
            /// <param name="Relationship"> Связь, из которой была получена вершина. </param>
            /// <returns> true, если для заданной вершины механизм обхода графа должен быть остановлен, иначе false. </returns>
            public override bool OnVisited(EntityObject TargetEntity, EntityObject SourceEntity, IRelatedEnd Relationship)
            {
                var casted = (DomainObject)TargetEntity;
                if (casted.Context == context) return true; // контекст уже проставлен, следует остановиться
                casted.Context = context;
                return false;
            }
        }

        /// <summary> Посетитель-перечислитель графа объектов. </summary>
        /// <remarks> Перечислитель сохраняет все найденные посещённые вершины графа в своём списке. </remarks>
        private class EnumerateContextVisitor : EntityObjectGraphVisitor
        {
            private readonly List<EntityObject> graph = new List<EntityObject>();
            internal List<EntityObject> Graph { get { return graph; } }
            /// <summary> Семантика посещения вершины графа. </summary>
            /// <param name="TargetEntity"> Посещаемая вершина. </param>
            /// <param name="SourceEntity"> Вершина, с которой был переход на посещаемую вершину. </param>
            /// <param name="Relationship"> Связь, из которой была получена вершина. </param>
            /// <returns> true, если для заданной вершины механизм обхода графа должен быть остановлен, иначе false. </returns>
            public override bool OnVisited(EntityObject Entity, EntityObject SourceEntity, IRelatedEnd Relationship)
            {
                if (graph.Contains(Entity)) return true;
                graph.Add(Entity);
                return false;
            }
        }

        #endregion

        #region Поля

        /// <summary> Словарь для связей внешних интерфейсов сервисов запросов с их внутренней реализацией. </summary>
        private Dictionary<Type, object> queryHelpersMapping = new Dictionary<Type, object>();
        /// <summary> Вызывать метод <c>ObjectContext.ExecuteFunction</c> приходится через рефлексию. </summary>
        private static MethodInfo execMethod = typeof(ZOVNPContext).GetMethod("ExecuteFunction", new[] { typeof(string), typeof(MergeOption), typeof(ObjectParameter[]) });
        //private static MethodInfo findOrAttachMethod = typeof(DataExtensions).GetMethod("FindOrAttach", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
        private object queryHelpersMappingLock = new object();
        private ContextAdder adder;
        private ContextGetter getter;
        /// <summary> Признак того, что контекст уже был уничтожен. </summary>
        private volatile int disposed;

        #endregion

        #region Свойства

        /// <summary> Ссылка на движок, который работает с данным контекстом базы данных и породил его. </summary>
        IDomainEngine IDomainContext.Engine { get { return Engine; } }

        /// <summary> Ссылка на движок, который работает с данным контекстом базы данных и породил его. </summary>
        internal ZOVNPEngine Engine { get { return (ZOVNPEngine)engine; } }

        /// <summary> Менеджер занесения новых доменных объектов в данный контекст. </summary>
        internal ContextAdder Adder { get { return adder; } }

        /// <summary> Менеджер занесения новых доменных объектов в данный контекст. </summary>
        IContextAdder IDomainContext.Adder { get { return adder; } }

        /// <summary> Менеджер получения данных. </summary>
        public IContextGetter Getter { get { return getter; } }

        #endregion

        #region Методы

        /// <summary> Этот метод вызывается в конструкторе данного класса. </summary>
        partial void OnContextCreated()
        {
            OnCreated();
            ObjectMaterialized += ObjectMaterializedHandler;
            adder = new ContextAdder(this);
            getter = new ContextGetter(this);
        }

        /// <summary> Сохранение всех изменений модели, сделанных в контексте движка, в хранилище данных. </summary>
        int IDomainContext.SaveChanges()
        {
            GuardDispose();
            int num = 0;
            // TODO: изменить текст сообщения
            if (QueryCache<IUser>().Any(it => it.EntityState != EntityState.Unchanged))
                throw new InvalidOperationException(
                    "Сохранение контекста повлечет за собой сохранение свойств субъектов доступа. Они должны сохраняться специальной процедурой.");
            try
            {
                //
                var added = this.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Added);
                var changed = this.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Modified);
                //QueryCache<ICustomer>().Where(c => ((DomainObject) c).New).ToArray();
                //
                foreach (var entry in added.Union(changed))
                {
                    if (!((IDomainObject) entry.Entity).CheckSelfProperties())
                    {
                        throw new NotNullableFieldIsNullEntityExceptionException((IDomainObject) entry.Entity,
                                                                                 string.Format("Незаполнены все обязательные поля у объекта. {0}",
                                                                                               entry.Entity.ToString()));
                    }
                }

                num = SaveChanges();

                //
                foreach (var domainObject in added)
                {
                    ((DomainObject) domainObject.Entity).New = false;
                }
                //
            }

            catch (OptimisticConcurrencyException Ex)
            {
                foreach (var entry in Ex.StateEntries)
                {
                    //    entry
                }
                DebugTuner.Instance.Break();
            }
            catch (UpdateException Ex)
            {
                DebugTuner.Instance.Break();
                throw new InvalidOperationException(Ex.ToLongString());
            }
            catch (NotNullableFieldIsNullEntityExceptionException ex)
            {
                throw;
            }
            catch (Exception)
            {

                DebugTuner.Instance.Break();
            }
            return num;
        }

        /// <summary> Обработчик создания доменного объекта. </summary>
        /// <param name="sender"> Источник события - контекст. </param>
        /// <param name="e"> Параметры события, содержащие созданный объект. </param>
        private void ObjectMaterializedHandler(object sender, ObjectMaterializedEventArgs e)
        {
            var casted = (DomainObject)e.Entity;
            casted.Context = this;
        }

        /// <summary> Уничтожение контекста. </summary>
        /// <param name="Disposing"> Флаг освобождения управляемых ресурсов. </param>
        protected override void Dispose(bool Disposing)
        {
#pragma warning disable 420
            if (!Disposing || System.Threading.Interlocked.CompareExchange(ref disposed, 1, 0) != 0) return;
#pragma warning restore 420

            using (new DisposeHelper(this))
            {
                // В текущем релизе EF контекст после своего уничтожения не переводит состояния своих сущностей в System.Data.EntityState.Detached.
                // Если это не делать самим, то будут ошибки. Делать это надо. Microsoft в следующем релизе обещают это исправить.
                // Ранее я это делал с помощью вызова метода Detach, но тогда разрушаются все навигационные связи.
                // Вызов метода SetChangeTracker(null) оставляет все навигационные связи в том состоянии, в котором они были.
                foreach (var entry in ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Added |
                    System.Data.EntityState.Deleted | System.Data.EntityState.Modified | System.Data.EntityState.Unchanged))
                {
                    var casted = (DomainObject)entry.Entity;
                    ((IEntityWithChangeTracker)casted).SetChangeTracker(null);
                    //Detach(casted);
                    casted.Context = null;
                    DebugTuner.Instance.Assert(casted.EntityState == System.Data.EntityState.Detached, "Неверное состояние объекта");
                }
                ObjectMaterialized -= ObjectMaterializedHandler;
                foreach (var query in queryHelpersMapping.Values)
                {
                    var disposable = query as IDisposable;
                    if (disposable != null) disposable.Dispose();
                }
                queryHelpersMapping.Clear();

                base.Dispose(Disposing);
            }
        }

        private static void ThrowQueryServiceNotSupported(Type InterfaceType)
        {
            throw new ServiceNotSupportedException(typeof(IObjectQuery<>).MakeGenericType(InterfaceType),
                string.Format(Properties.Resources.MSG_QueryServiceNotSupported, InterfaceType.Name, InterfaceType.Name.Substring(1, InterfaceType.Name.Length - 1)));
        }

        private object TryBuildObjectQuery(Type InterfaceType)
        {
            GuardDispose();
            InterfaceType.CheckOnNull("InterfaceType");
            if (!InterfaceType.IsInterface) throw new System.ArgumentException(Properties.Resources.MSG_OnlyInterfaces);
            Type implType = null;
            if (!ZOVNPMetadata.TypeMapping.TryGetForwardValue(InterfaceType, out implType))
                ThrowQueryServiceNotSupported(InterfaceType);
            object typedQuery = null;
            lock (queryHelpersMappingLock)
            {
                if (!queryHelpersMapping.TryGetValue(InterfaceType, out typedQuery))
                {
                    PropertyInfo queryProp;
                    if (ZOVNPMetadata.ObjectSets.TryGetValue(InterfaceType, out queryProp))
                    {
                        typedQuery = typeof(ZOVNPObjectQuery<,>).MakeGenericType(
                            InterfaceType, implType).GetConstructor(new System.Type[] { typeof(ObjectQuery), typeof(bool) }).
                            Invoke(new object[] { (System.Data.Objects.ObjectQuery)queryProp.GetValue(this, null), false });
                    }
                    else // Свойства может не быть в случае унаследованных интерфейсов, например как в случае с IRole, IWindowUser для ISubject.
                    {
                        var attrs = implType.GetCustomAttributes(typeof(DomainObjectAttribute), true).Cast<DomainObjectAttribute>().ToArray();
                        if (attrs.Length != 0 && attrs[0].DomainObjectBaseInterfaceType != null) // задан базовый интерфейс?
                        {
                            var baseQuery = TryBuildObjectQuery(attrs[0].DomainObjectBaseInterfaceType); // опорный ObjectQuery для базового интерфейса
                            typedQuery = baseQuery.GetType().GetMethod("OfType").MakeGenericMethod(InterfaceType).Invoke(baseQuery, null); // фильтрация по унаследованному типу
                        }
                        else ThrowQueryServiceNotSupported(InterfaceType);
                    }
                    queryHelpersMapping.Add(InterfaceType, typedQuery);
                }
                return typedQuery;
            }
        }

        /// <summary> Получить типизированный сервис для доступа к объектам хранилища. </summary>
        /// <typeparam name="T"> Тип объектов, возвращаемых сервисом. </typeparam>
        /// <returns> Сервис для доступа к объектам. </returns>
        /// <exception cref="ServiceNotSupportedException"> Сервис требуемого типа объектов не поддерживается. </exception>
        public IObjectQuery<T> Query<T>() where T : IDomainObject { return (IObjectQuery<T>)TryBuildObjectQuery(typeof(T)); }

        /// <summary> Получение всех доменных объектов заданного типа. </summary>
        /// <param name="DomainType"> Тип (должен быть типом интерфейса) доменных объектов. </param>
        /// <returns> Массив объектов заданного типа. </returns>
        public IDomainObject[] PlainQuery(Type DomainType) { return ((IPlainQuery)TryBuildObjectQuery(DomainType)).ToArray(); }

        /// <summary> Получение доменного объекта заданного типа. </summary>
        /// <param name="DomainType"> Тип (должен быть типом интерфейса) доменного объекта. </param>
        /// <param name="ID"> Идентификатор доменного объекта </param>
        /// <returns> Экземпляр доменного объекта или null, если такового не найдено. </returns>
        public IDomainObject PlainQuery(Type DomainType, Guid ID) { return ((IPlainQuery)TryBuildObjectQuery(DomainType)).One(ID); }

        /// <summary> Получение доменного объекта из локального кэша контекста. </summary>
        /// <typeparam name="T"> Тип доменного объекта. </typeparam>
        /// <param name="Entity"> Этот же доменный объект, полученный из неизвестного контекста. </param>
        /// <param name="Option"> Опции запроса, определяющие поведение в случае отсутствия требуемого объекта. </param>
        /// <returns> Объект-близнец заданного, если он есть в кэше. </returns>
        /// <remarks> Локальный кэш контекста формируется в результате выполняемых в рамках этого контекста запросов. </remarks>
        public T QueryCache<T>(T Entity, QueryCacheOption Option) where T : class, IDomainObject
        {
            GuardDispose();
            Entity.CheckOnNull("Entity");
            if (!typeof(T).IsInterface) throw new ArgumentException(Properties.Resources.MSG_OnlyInterfaceTemplateParam);
            var casted = Entity.CheckOnCast<DomainObject>("Entity");
            ObjectStateEntry entry;
            ObjectStateManager.TryGetObjectStateEntry(casted.EntityKey, out entry);
            if (entry != null)
                return (T)entry.Entity;
            switch (Option)
            {
                case QueryCacheOption.None: return null;
                case QueryCacheOption.Attach:
                    if (casted.Context != null) // casted.Context заведомо не равен this, потому что эта ситуация обработана выше
                        throw new InvalidOperationException("Объект может быть присоединен не более, чем к одному контексту.");
                    AttachObject(Entity);
                    return Entity;
                case QueryCacheOption.Query: return Query<T>().One(Entity.ID);
                default: throw new NotImplementedException();
            }
        }

        /// <summary> Получение доменного объекта из локального кэша контекста. </summary>
        /// <typeparam name="T"> Тип доменного объекта. </typeparam>
        /// <param name="ID"> Идентификатор доменного объекта. </param>
        /// <param name="Option"> Опции запроса, определяющие поведение в случае отсутствия требуемого объекта. </param>
        /// <returns> Требуемый доменный объект. </returns>
        /// <remarks> Локальный кэш контекста формируется в результате выполняемых в рамках этого контекста запросов. </remarks>
        public T QueryCache<T>(Guid ID, QueryCacheOption Option) where T : class, IDomainObject
        {
            GuardDispose();
            if (!typeof(T).IsInterface) throw new ArgumentException(Properties.Resources.MSG_OnlyInterfaceTemplateParam);
            ObjectStateEntry entry;
            var key = new EntityKey(string.Format("{0}.{1}", DefaultContainerName, ZOVNPMetadata.TypeMapping.Forward(typeof(T)).Name), "ID", ID);
            ObjectStateManager.TryGetObjectStateEntry(key, out entry);
            if (entry != null)
                return (T)entry.Entity;
            switch (Option)
            {
                case QueryCacheOption.None: return null;
                case QueryCacheOption.Attach: throw new ArgumentException("Опция QueryCacheOption.Attach не имеет смысла для данной перегрузки метода.");
                case QueryCacheOption.Query: return Query<T>().One(ID);
                default: throw new NotImplementedException();
            }
        }

        /// <summary> Получение объектов из локального кэша контекста. </summary>
        /// <typeparam name="T"> Тип доменного объекта. </typeparam>
        /// <returns> Коллекция доменных объектов. </returns>
        /// <remarks> Локальный кэш контекста формируется в результате выполняемых в рамках этого контекста запросов. </remarks>
        public T[] QueryCache<T>()
        {
            return ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Unchanged | System.Data.EntityState.Modified | System.Data.EntityState.Added | System.Data.EntityState.Deleted).
                Select(it => it.Entity).OfType<T>().ToArray();
        }

        /// <summary> Получение объектов из локального кэша контекста. </summary>
        /// <typeparam name="T"> Тип доменного объекта. </typeparam>
        /// <param name="Predicate"> Предикат для отбора объектов. </param>
        /// <returns> Коллекция доменных объектов. </returns>
        /// <remarks> Локальный кэш контекста формируется в результате выполняемых в рамках этого контекста запросов. </remarks>
        public T[] QueryCache<T>(Func<T, bool> Predicate)
        {
            return ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Unchanged | System.Data.EntityState.Modified | System.Data.EntityState.Added | System.Data.EntityState.Deleted).
                Select(it => it.Entity).OfType<T>().Where(Predicate).ToArray();
        }

        private static void ReadBack(Fintech.Utils.Database.Parameter[] Parameters, ObjectParameter[] ObjectParameters)
        {
            if (Parameters.Length == 0) return;
            if (Parameters.Length != ObjectParameters.Length) throw new ArgumentException("Parameters/ObjectParameters");
            for (int i = 0; i < Parameters.Length; ++i)
                if (Parameters[i].Direction == Fintech.Utils.Database.ParameterDirection.Output || Parameters[i].Direction == Fintech.Utils.Database.ParameterDirection.InputOutput)
                    Parameters[i].Value = ObjectParameters[i].Value;
        }

        private static void ReadBack(Fintech.Utils.Database.Parameter[] Parameters, DbParameter[] DbParameters)
        {
            if (Parameters.Length == 0) return;
            if (Parameters.Length != DbParameters.Length) throw new ArgumentException("Parameters/DbParameters");
            for (int i = 0; i < Parameters.Length; ++i)
                if (Parameters[i].Direction == Fintech.Utils.Database.ParameterDirection.Output || Parameters[i].Direction == Fintech.Utils.Database.ParameterDirection.InputOutput)
                    Parameters[i].Value = DbParameters[i].Value;
        }

        private static void CheckNullParams(Fintech.Utils.Database.Parameter[] Parameters, string FunctionName)
        {
            // переданные параметры нужно проверить на null, так как легко ошибиться
            for (int i = 0; i < Parameters.Length; ++i)
                if (Parameters[i] == null)
                    throw new ArgumentNullException("Parameters", string.Format(Properties.Resources.MSG_NullParamPassed, i, FunctionName));
#if DEBUG
                else if (Parameters[i].Type == typeof(Guid))
                {
                    if (Parameters[i].Value != null && (Guid)Parameters[i].Value == Guid.Empty)
                        DebugTuner.Instance.Break();
                }
#endif
        }

        /// <summary> Метод либо находит уже существующую в контексте сущность по ключу переданной сущности, либо присоединяет к контексту переданную. </summary>
        /// <param name="Entity"> Сущность, которую необходимо занести в контекст. </param>
        /// <returns> Сущность, занесенная в контекст, либо находящаяся уже там. Во втором случае возвращаемая ссылка на объект отличается от переданной. </returns>
        public object FindOrAttach(object Entity)
        {
            Entity.CheckOnNull("Entity");
            string qualifiedEntitySetName = typeof(ZOVNPContext).Name + "." + Entity.GetType().Name; //TODO: это допущение, безопаснее (но медленнее и неудобно) делать objectSet.EntitySet.EntityContainer.Name + "." + objectSet.EntitySet.Name;
            EntityKey entityKey = CreateEntityKey(qualifiedEntitySetName, Entity);
            ObjectStateEntry existingStateEntry;
            if (ObjectStateManager.TryGetObjectStateEntry(entityKey, out existingStateEntry) && existingStateEntry.Entity != null) // A proxy entry may exist for the entity instance
            {
                if (existingStateEntry.Entity is ICopyValues)
                    (existingStateEntry.Entity as ICopyValues).CopyFrom(Entity);
                return (DomainObject)existingStateEntry.Entity;
            }
            else
            {
                AttachTo(qualifiedEntitySetName, Entity);
                ((DomainObject)Entity).Context = this;
                return Entity;
            }
        }

        private T[] ExecuteProcedureInner<T>(string FunctionName, IMaterializer Materializer, params Fintech.Utils.Database.Parameter[] Parameters)
        {
            GuardDispose();

            if (Materializer != null) // задан нестандартный материализатор
                return (T[])ExecuteProcedureInner(FunctionName, new[] { MaterializerEntry.Create<T>(Materializer) }, Parameters)[0];

            // со стандартным материализатором будет вестись работа через API Entity Framework

            var t = typeof(T);
            if (t.IsInterface)
            {
                if (!ZOVNPMetadata.TypeMapping.TryGetForwardValue(t, out t))
                    throw new System.NotSupportedException(string.Format(Properties.Resources.MSG_NotSupportedFunction, FunctionName, (t == null ? typeof(T) : t).FullName));
            }
            else throw new NotSupportedException(string.Format(Properties.Resources.MSG_NotSupportedFunction, FunctionName, (t == null ? typeof(T) : t).FullName));

            CheckNullParams(Parameters, FunctionName);

            while (true)
            {
                var objectParams = Parameters.Select(it => it.Value != null ? new ObjectParameter(it.Name, it.Value) : new ObjectParameter(it.Name, it.Type)).ToArray();
                DebugTuner.Instance.Logger.WriteLine(() => engine.ProviderAdapter.ToTraceString(FunctionName, CommandType.StoredProcedure, objectParams));
                bool entered = false; // признак удачного захвата блокировки
                try
                {
                    engine.EnterDatabaseCriticalSection(out entered);
                    return ((IEnumerable<T>)execMethod.MakeGenericMethod(t).Invoke(this, new object[] { FunctionName, MergeOption.OverwriteChanges, objectParams })).ToArray();
                }
                catch (NestedQueryInTheSameThreadException) { entered = false; throw; }
                catch (System.Threading.ThreadAbortException) { } // ошибку от Thread.Abort() нужно пропускать наверх
                catch (System.Reflection.TargetInvocationException Ex)
                {
                    if (!engine.HandleException(Ex, this))
                        throw;
                }
                finally
                {
                    if (entered) engine.ExitDatabaseCriticalSection();
                    ReadBack(Parameters, objectParams);
                }
            }
        }

        internal Array[] ExecuteProcedureInner(string FunctionName, MaterializerEntry[] Entries, params Fintech.Utils.Database.Parameter[] Parameters)
        {
            GuardDispose();
            foreach (var entry in Entries)
            {
                Type impl = entry.EntityType;
                if (entry.EntityType.IsInterface)
                {
                    if (!ZOVNPMetadata.TypeMapping.TryGetForwardValue(entry.EntityType, out impl))
                        throw new System.NotSupportedException(string.Format(Properties.Resources.MSG_NotSupportedFunction, FunctionName, (impl == null ? entry.EntityType : impl).FullName));
                }
                else throw new NotSupportedException(string.Format(Properties.Resources.MSG_NotSupportedFunction, FunctionName, (impl == null ? entry.EntityType : impl).FullName));
            }

            CheckNullParams(Parameters, FunctionName);

            while (true)
            {
                var nativeParams = Parameters.Select(it => Engine.ParameterProvider.CreateParam(it.Name, it.Value, it.Direction)).ToArray();
                using (var command = this.CreateStoreCommand(FunctionName, CommandType.StoredProcedure, nativeParams))
                {
                    var result = new Array[Entries.Length];
                    DebugTuner.Instance.Logger.WriteLine(() => engine.ProviderAdapter.ToTraceString(FunctionName, CommandType.StoredProcedure, nativeParams));
                    bool entered = false; // признак удачного захвата блокировки
                    try
                    {
                        engine.EnterDatabaseCriticalSection(out entered);
                        using (command.Connection.CreateConnectionScope())
                        using (var reader = command.ExecuteReader())
                        {
                            for (int i = 0; i < Entries.Length; ++i)
                            {
                                var materializer = /*Entries[i].IsDefault ? StandardMaterializers.Materializers[Entries[i].EntityType] : */Entries[i].Materializer;
                                if (materializer == null) throw new ArgumentException("Материализатор не найден.");
                                var entities = materializer.Materialize(reader).ToArray();
                                for (int j = 0; j < entities.Length; ++j) entities[j] = FindOrAttach(entities[j]);
                                var copy = Array.CreateInstance(Entries[i].EntityType, entities.Length);
                                Array.Copy(entities, copy, entities.Length);
                                result[i] = copy;
                                if (!reader.NextResult()) break;
                            }
                        }
                        return result;
                    }
                    catch (NestedQueryInTheSameThreadException) { entered = false; throw; }
                    catch (System.Threading.ThreadAbortException) { } // ошибку от Thread.Abort() нужно пропускать наверх
                    catch (System.Reflection.TargetInvocationException Ex)
                    {
                        if (!engine.HandleException(Ex, this))
                            throw;
                    }
                    finally
                    {
                        if (entered) engine.ExitDatabaseCriticalSection();
                        ReadBack(Parameters, nativeParams);
                    }
                }
            }
        }

        /// <summary> Выполнить функцию, возвращающую набор сущностей. </summary>
        /// <typeparam name="T"> Тип возвращаемых функцией сущностей. </typeparam>
        /// <param name="FunctionName"> Имя функции (из схемы EDMX). </param>
        /// <param name="Parameters"> Параметры для функции. </param>
        /// <returns> Массив объектов. </returns>
        public T[] ExecuteProcedure<T>(string FunctionName, params Fintech.Utils.Database.Parameter[] Parameters)
        {
            return ExecuteProcedureInner<T>(FunctionName, null, Parameters);
        }

        /// <summary> Выполнить команду над хранилищем, возвращающую произвольное число типизированных наборов объектов. </summary>
        /// <param name="FunctionName"> Название функции (команды). </param>
        /// <param name="Types"> Массив типов, наборы которых должны быть возвращены. </param>
        /// <param name="Parameters">  Параметры команды. </param>
        /// <returns> </returns>
        public object[] ExecuteProcedure(string FunctionName, Type[] Types, params Fintech.Utils.Database.Parameter[] Parameters)
        {
            return ExecuteProcedureInner(FunctionName, Types.ConvertAll(it => MaterializerEntry.Default(it)), Parameters);
        }

        /// <summary> Выполнить команду над хранилищем, возвращающую 2 типизированных набора объектов. </summary>
        /// <typeparam name="T"> Тип первого набора. </typeparam>
        /// <typeparam name="U"> Тип второго набора. </typeparam>
        /// <param name="FunctionName"> Название функции (команды). </param>
        /// <param name="Parameters"> Параметры команды. </param>
        /// <returns> Кортеж массивов возвращённых командой объектов - результат выполнения команды.  </returns>
        /// <remarks> Выполнение команды по сути есть выполнение хранимой процедуры в хранилище. </remarks>
        public Tuple<T[], U[]> ExecuteProcedure<T, U>(string FunctionName, params Fintech.Utils.Database.Parameter[] Parameters)
        {
            var result = ExecuteProcedure(FunctionName, new Type[] { typeof(T), typeof(U) }, Parameters);
            return new Tuple<T[], U[]>((T[])result[0], (U[])result[1]);
        }

        /// <summary> Выполнить команду над хранилищем, возвращающую 3 типизированных набора объектов. </summary>
        /// <typeparam name="T"> Тип первого набора. </typeparam>
        /// <typeparam name="U"> Тип второго набора. </typeparam>
        /// <typeparam name="K"> Тип третьего набора. </typeparam>
        /// <param name="FunctionName"> Название функции (команды). </param>
        /// <param name="Parameters"> Параметры команды. </param>
        /// <returns> Кортеж массивов возвращённых командой объектов - результат выполнения команды.  </returns>
        /// <remarks> Выполнение команды по сути есть выполнение хранимой процедуры в хранилище. </remarks>
        public Tuple<T[], U[], K[]> ExecuteProcedure<T, U, K>(string FunctionName, params Fintech.Utils.Database.Parameter[] Parameters)
        {
            var result = ExecuteProcedure(FunctionName, new Type[] { typeof(T), typeof(U), typeof(K) }, Parameters);
            return new Tuple<T[], U[], K[]>((T[])result[0], (U[])result[1], (K[])result[2]);
        }

        /// <summary> Выполнить команду над хранилищем, возвращающую 4 типизированных набора объектов. </summary>
        /// <typeparam name="T"> Тип первого набора. </typeparam>
        /// <typeparam name="U"> Тип второго набора. </typeparam>
        /// <typeparam name="K"> Тип третьего набора. </typeparam>
        /// <typeparam name="V"> Тип четвертого набора. </typeparam>
        /// <param name="FunctionName"> Название функции (команды). </param>
        /// <param name="Parameters"> Параметры команды. </param>
        /// <returns> Кортеж массивов возвращённых командой объектов - результат выполнения команды.  </returns>
        /// <remarks> Выполнение команды по сути есть выполнение хранимой процедуры в хранилище. </remarks>
        public Tuple<T[], U[], K[], V[]> ExecuteProcedure<T, U, K, V>(string FunctionName, params Fintech.Utils.Database.Parameter[] Parameters)
        {
            var result = ExecuteProcedure(FunctionName, new Type[] { typeof(T), typeof(U), typeof(K), typeof(V) }, Parameters);
            return new Tuple<T[], U[], K[], V[]>((T[])result[0], (U[])result[1], (K[])result[2], (V[])result[3]);
        }

        /// <summary> Обновить объект, полученный ранее из источника данных. </summary>
        /// <param name="Entity"> Обновляемый объект. </param>
        /// <param name="KeepEntityChanges"> True, если требуется не перезаписывать изменённые свойства объекта. </param>
        public void Refresh(object Entity, bool KeepEntityChanges = false)
        {
            GuardDispose();
            var casted = Entity.CheckOnCast<DomainObject>("Entity");
            if (casted.EntityState != System.Data.EntityState.Unchanged && casted.EntityState != System.Data.EntityState.Modified)
                throw new InvalidOperationException(string.Format("Обновляемый объект находится в состоянии {0}.", casted.EntityState));
            while (true)
            {
                bool entered = false; // признак удачного захвата блокировки
                try
                {
                    engine.EnterDatabaseCriticalSection(out entered);
                    Refresh(KeepEntityChanges ? RefreshMode.ClientWins : RefreshMode.StoreWins, Entity);
                    var cacheable = Entity as ICacheableDomainObject;
                    if (cacheable != null) cacheable.RefreshAdditionalCache();
                    return;
                }
                catch (NestedQueryInTheSameThreadException) { entered = false; throw; }
                catch (System.Threading.ThreadAbortException) { } // Thread.Abort() нужно пропускать наверх
                catch (System.Exception Ex)
                {
                    if (!engine.HandleException(Ex, this))
                        throw;
                }
                finally { if (entered) engine.ExitDatabaseCriticalSection(); }
            }
        }

        /// <summary> Присоединить объект к контексту. </summary>
        /// <param name="Object"> Присоединяемый объект. </param>
        /// <param name="BreakExistingRelations"> true, если к контексту следует присоединить только указанный объект без присоединения его навигационных связей, иначе false. </param>
        /// <remarks> Если объект уже присоединен к этому контексту, то метод не производит никакого эффекта. </remarks>
        /// <exception cref="InvalidOperationException"> Объект уже связан с другим контекстом либо имеет состояние, отличное от состояния <c>Domain.EntityState.Detached</c>. </exception>
        public void AttachObject(IDomainObject Object, bool BreakExistingRelations = true)
        {
            GuardDispose();
            var casted = Object.CheckOnCast<DomainObject>("Object");
            if (object.ReferenceEquals(Object.Context, this))
                return; // доменный объект уже присоединен к этому контексту
            if (!object.ReferenceEquals(Object.Context, null) && !object.ReferenceEquals(Object.Context, this))
                throw new InvalidOperationException(string.Format("Присоединяемый к контексту №{0} объект уже связан с контекстом №{1}.", Index, ((ZOVNPContext)Object.Context).Index));
            if (Object.EntityState == EntityState.Detached)
            {
#if DEBUG
                //var profiler = new EnumerateContextVisitor();
                //profiler.Visit(casted);
                //var entitiesToAdd = profiler.Graph;
#endif
                Attach(casted);
                new AssignContextVisitor(this).Visit(casted);
            }
            else throw new InvalidOperationException("Объект должен находиться в отсоединенном от контекста состоянии.");
        }

        /// <summary> Отсоединить объект от контекста. </summary>
        /// <param name="Object"> Отсоединяемый объект. </param>
        /// <remarks> Если объект уже отсоединен от контекста, то метод не производит никакого эффекта. </remarks>
        /// <exception cref="InvalidOperationException"> Объект уже связан с другим контекстом. </exception>
        public void DetachObject(IDomainObject Object)
        {
            GuardDispose();
            var casted = Object.CheckOnCast<DomainObject>("Object");
            if (casted.Context != this && casted.Context != null)
                throw new InvalidOperationException("Отсоединяемый от контекста объект не принадлежит этому контексту.");
            if (Object.EntityState != EntityState.Detached && Object.EntityState != EntityState.Deleted)
            {
                Detach(casted);
                casted.Context = null;
            }
        }

        /// <summary> Удалить объект из хранилища. </summary>
        /// <param name="Object"> Удаляемый объект. </param>
        public void Delete(IDomainObject Object)
        {
            GuardDispose();
            var casted = Object.CheckOnCast<DomainObject>("Object");
            if (casted.Context != this && casted.Context != null)
                throw new InvalidOperationException("Удаляемый из контекста объект не принадлежит этому контексту.");
            if (Object.EntityState != EntityState.Detached && Object.EntityState != EntityState.Deleted)
            {
                DeleteObject(Object);
                casted.Context = null;
            }
        }

        /// <summary> Добавить в хранилище новый доменный объект. </summary>
        /// <param name="Domain"> Объект для добавления к хранилищу. </param>
        public void Add(IDomainObject Domain)
        {
            GuardDispose();
            var type = Domain.GetType();
            string methodName = "AddTo" + type.Name + "s";
            var method = GetType().GetMethod(methodName, new[] { type });
            if (method == null) throw new InvalidOperationException(string.Format(Properties.Resources.MSG_InvalidTypesForAdd, type));
            method.Invoke(this, new[] { Domain });
        }

        #endregion
    }
}
